package main

import (
	"fmt"
	"github.com/pkg/errors"
	"go/ast"
	"go/parser"
	"go/token"
	"gocamp2022/gen/annotation"
	"gocamp2022/gen/http"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"unicode"
)

// 实际上 main 函数这里要考虑接收参数
// src 源目标
// dst 目标目录
// type src 里面可能有很多类型，那么用户可能需要指定具体的类型
// 这里我们简化操作，只读取当前目录下的数据，并且扫描下面的所有源文件，然后生成代码
// 在当前目录下运行 go install 就将 main 安装成功了，
// 可以在命令行中运行 gen
// 在 testdata 里面运行 gen，则会生成能够通过所有测试的代码
func main() {
	err := gen(".")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println("success")
}

func gen(src string) error {
	// 第一步找出符合条件的文件
	srcFiles, err := scanFiles(src)
	if err != nil {
		return err
	}
	// 第二步，AST 解析源代码文件，拿到 service definition 定义
	defs, err := parseFiles(srcFiles)
	if err != nil {
		return err
	}
	// 生成代码
	return genFiles(src, defs)
}

// 根据 defs 来生成代码
// src 是源代码所在目录，在测试里面它是 ./testdata
func genFiles(src string, defs []http.ServiceDefinition) error {
	for _, def := range defs {
		filename := filepath.Join(src, def.Name+"_gen.go")
		f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
		if err != nil {
			return errors.Wrap(err, "创建文件失败")
		}

		err = http.Gen(f, &def)

		if err != nil {
			return errors.Wrap(err, "生成代码失败")
		}
	}

	return nil
}

func parseFiles(srcFiles []string) ([]http.ServiceDefinition, error) {
	fmt.Printf("parse files: %v", srcFiles)
	defs := make([]http.ServiceDefinition, 0, 20)
	for _, src := range srcFiles {
		// 你需要利用 annotation 里面的东西来扫描 src，然后生成 file
		// panic("implement me")
		// 解析文件
		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, src, nil, parser.ParseComments)

		if err != nil {
			panic(err)
		}

		tv := &annotation.SingleFileEntryVisitor{}
		ast.Walk(tv, f)
		file := tv.Get()

		for _, typ := range file.Types {
			_, ok := typ.Annotations.Get("HttpClient")
			if !ok {
				continue
			}
			def, err := parseServiceDefinition(file.Node.Name.Name, typ)
			if err != nil {
				return nil, err
			}
			defs = append(defs, def)
		}
	}
	return defs, nil
}

// 你需要利用 typ 来构造一个 http.ServiceDefinition
// 注意你可能需要检测用户的定义是否符合你的预期
func parseServiceDefinition(pkg string, typ annotation.Type) (http.ServiceDefinition, error) {
	// panic("implement me")

	def := http.ServiceDefinition{
		Package: pkg,
	}

	// 服务名称
	def.Name = typ.Node.Name.Name

	// 请求类型 GET POST PUT...
	methodP, _ := typ.Annotations.Get("Method")

	// 方法列表
	for _, mth := range typ.Fields {
		pathT, _ := mth.Annotations.Get("Path")
		methodM, _ := mth.Annotations.Get("Method")
		// 请求类型
		requestMehtod := methodM.Value

		if methodM.Value == "" {
			requestMehtod = methodP.Value
		}

		ft, ok := mth.Annotations.Node.Type.(*ast.FuncType)
		if !ok {
			continue
		}

		// 默认第一个参数是 context.Context
		if len(ft.Params.List) < 2 {
			return def, errMethodParamsWrong
		}

		// 第二个参数是 request
		req := ft.Params.List[1]

		// 默认第一个返回值是 response，第二个是 error
		if len(ft.Results.List) < 2 {
			return def, errSecondReturnNotError
		}

		resp := ft.Results.List[0]

		sm := http.ServiceMethod{
			Name:         mth.Node.Names[0].Name,
			Path:         pathT.Value,
			ReqTypeName:  req.Type.(*ast.StarExpr).X.(*ast.Ident).Name,
			RespTypeName: resp.Type.(*ast.StarExpr).X.(*ast.Ident).Name,
			ReqMethod:    requestMehtod,
		}

		def.Methods = append(def.Methods, sm)
	}

	return def, nil
}

// 返回符合条件的 Go 源代码文件，也就是你要用 AST 来分析这些文件的代码
func scanFiles(src string) ([]string, error) {
	var result []string

	basePath := src
	basePath = filepath.Clean(basePath)

	err := filepath.Walk(src, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return errors.Wrap(err, "遍历文件失败")
		}

		if info.IsDir() {
			return nil
		}

		if filepath.Ext(path) != ".go" {
			return nil
		}

		// 如果不是一级目录，就不要了
		if filepath.Dir(path) != basePath {
			return nil
		}

		if strings.HasSuffix(path, "_gen.go") {
			return nil
		}

		result = append(result, path)
		return nil
	})

	return result, err
}

// underscoreName 驼峰转字符串命名，在决定生成的文件名的时候需要这个方法
// 可以用正则表达式，然而我写不出来，我是正则渣
func underscoreName(name string) string {
	var buf []byte
	for i, v := range name {
		if unicode.IsUpper(v) {
			if i != 0 {
				buf = append(buf, '_')
			}
			buf = append(buf, byte(unicode.ToLower(v)))
		} else {
			buf = append(buf, byte(v))
		}

	}
	return string(buf)
}

var (
	errSecondReturnNotError = errors.New("gen: 方法必须返回两个参数，其中第一个返回值是响应，第二个返回值是error")
	errMethodParamsWrong    = errors.New("gen: 方法必须接收两个参数，其中第一个参数是 context.Context，第二个参数请求")
)
